import {protoMapping} from "./proto/mapping";
import {BasicMessage} from "./BasicMessage";
import {wxUtils} from "../wx/wxUtils";
import {utils} from "../utils/utils";

export class SocketIO {
  private websocket: WebSocket;
  private _isConnected: SocketState = SocketState.CLOSE;
  private msgStack = [];
  private msgMapping = new Map<{ new() }, Array<Function>>();

  constructor(private url: string) {
  }

  private callback: Function;
  private heartBeatId = -1;
  private params: any;

  connect(params?: any, callback?: Function) {
    if (!this.websocket || !this._isConnected) {
      this.params = params;
      this._isConnected = SocketState.CONNECTING;
      this.websocket = new WebSocket(this.getSocketUrl(params));
      this.websocket.onopen = this.onOpen.bind(this);
      this.websocket.onmessage = this.onMessage.bind(this);
      this.websocket.onclose = this.onclose.bind(this);
      this.websocket.onerror = this.onError.bind(this);
    }
    if (callback) {
      this.callback = callback;
    }
  }

  private onclose() {
    this._isConnected = SocketState.CLOSE;
    this.beginReconnect();
    clearInterval(this.heartBeatId);
  }

  private onError(err) {
    if (this.callback) {
      this.callback(err)
    }
    this._isConnected = SocketState.CLOSE;
    this.beginReconnect();
    clearInterval(this.heartBeatId);
  }

  private onOpen() {
    if (this.callback) {
      this.callback(null)
    }
    this._isConnected = SocketState.OPEN;

    this.msgStack.forEach(value => {
      this.emit(value[0], value[1]);
    });

    this.heartBeatId = setInterval(this.heartbeat.bind(this), 25000)
    this.callback = null;
    clearInterval(this.reconnectId)
    this.msgStack = []
  }

  private onMessage(ev: MessageEvent) {
    if (ev.data instanceof ArrayBuffer) {
      this.onResultReceived(ev.data)
    } else if (!wxUtils.isWechat && ev.data instanceof Blob) {
      const buffer: Blob = ev.data as Blob;
      const reader = new FileReader();
      reader.onload = () => {
        let result = <ArrayBuffer>reader.result;
        this.onResultReceived(result)
      };
      reader.readAsArrayBuffer(buffer)
    }
  }

  onResultReceived(result: ArrayBuffer) {
    const msg = BasicMessage.decode(result);
    const content = msg.content;
    if (content && msg.message !== 0) {
      cc.log(`received: ${msg.message} ${JSON.stringify(content.toJSON())}`)
      this.dispatchMessage(content)
    }
  }

  private dispatchMessage(content) {
    const callbacks = this.msgMapping.get(content.constructor)
    if (callbacks) {
      callbacks.forEach(callback => {
        callback(content)
      })
      this.msgMapping.set(content.constructor, callbacks.filter((value: any) => value.once != true))
    }
  }

  on<T>(type: { new(): T }, callback: Function) {
    if (!this.msgMapping.has(type)) {
      this.msgMapping.set(type, [])
    }
    this.msgMapping.get(type).push(callback);
  }

  off(type: any, callback: Function) {
    const callbacks = this.msgMapping.get(type);
    if (callbacks) {
      const deletes = callbacks.splice(callbacks.indexOf(callback), 1)
      return deletes.length === 1;
    }
    return false;
  }

  once<T>(type: { new(): T }, callback: Function) {
    if (!this.msgMapping.has(type)) {
      this.msgMapping.set(type, [])
    }
    callback['once'] = true;
    this.msgMapping.get(type).push(callback);
  }

  emit(msgId: number, obj: any) {
    if (this._isConnected !== SocketState.OPEN) {
      return this.msgStack.push([msgId, obj])
    }
    const protoClass = protoMapping.ID_TO_MSG.get(msgId);
    if (!protoClass) {
      throw new Error(`msgId: ${msgId} is not in proto`)
    }
    cc.log(`emit message ${msgId} ${JSON.stringify(obj.toJSON ? obj.toJSON() : obj)}`)
    const frame = new BasicMessage();
    frame.message = msgId;
    frame.content = protoClass.encode(obj).finish() as Uint8Array
    this.websocket.send(frame.encode())
  }

  get isConnected(): SocketState {
    return this._isConnected;
  }

  private reconnectCount = 0;
  private reconnectId: number = 0;

  private beginReconnect() {
    this.reconnectCount = 0;
    clearInterval(this.reconnectId)
    this.reconnectId = setInterval(() => {
      this.connect(this.params)
    }, 5000) as number
  }

  public static create(url: string): SocketIO {
    return new SocketIO(url);
  }

  heartbeat() {
    this.sendBasicCmd(0);
  }

  sendBasicCmd(msgid: number) {
    const frame = new BasicMessage();
    frame.message = msgid;
    frame.content = new Uint8Array(0)
    this.websocket.send(frame.encode());
  }

  getSocketUrl(params) {
    return "wss://est-api.shandw.com:7650/?" + utils.formatToQuery(params) + '&' + wxUtils.getQuery()
  }
}

export enum SocketState {
  CLOSE,
  CONNECTING,
  OPEN
}

